// 
//  Copyright © 2008, 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;
using Galaxium.Protocol.Xmpp.Library.Extensions;

using Anculus.Core;

namespace Galaxium.Protocol.Xmpp.Library
{
	public delegate void PresenceHandler (Contact item, Presence pres);
	public delegate void SubscriptionUpdateHandler (Contact item,
	                                                SubscriptionState old_state);

	public class Roster: IEnumerable<Contact>, IPresenceListener
	{
		private Client _client;
		
		private Dictionary<string, List<JabberID>> _groups =
			new Dictionary<string, List<JabberID>> ();
		
		private Dictionary<JabberID, Contact> _items =
			new Dictionary<JabberID, Contact> ();

		private Dictionary<JabberID, Presence> _requests =
			new Dictionary<JabberID, Presence> ();

		private Dictionary<JabberID, string> _avatar_hashes =
			new Dictionary<JabberID, string> ();
		
		public string[] Groups {
			get { return _groups.Keys.ToArray (); }
		}

		#region Events
		
		public event EventHandler<ContactPresenceEventArgs>     PresenceUpdated;
		public event EventHandler<SubscriptionRequestEventArgs> SubscriptionRequested;
		public event EventHandler<SubscriptionUpdateEventArgs>  SubscriptionUpdated;
		public event EventHandler<ContactEventArgs>      SubscriptionRequestDeclined;
		
		public event EventHandler<ContactEventArgs>      ContactUpdated;
		public event EventHandler<ContactEventArgs>      ContactAdded;
		public event EventHandler<ContactEventArgs>      ContactRemoved;
		public event EventHandler<ContactGroupEventArgs> ContactAddedToGroup;
		public event EventHandler<ContactGroupEventArgs> ContactRemovedFromGroup;
		public event EventHandler<ContactNameEventArgs>  ContactNameChanged;
		public event EventHandler<AvatarChangeEventArgs> ContactAvatarChanged;
		
		public event EventHandler ContactsReceived;
		public event EventHandler RetrievingFailed;
		

		protected void OnPresenceUpdated (Contact contact, Presence presence)
		{
			if (PresenceUpdated != null)
				PresenceUpdated (this, new ContactPresenceEventArgs (contact, presence));
		}
		
		protected void OnSubscriptionRequested (Contact contact, string message)
		{
			if (SubscriptionRequested != null)
				SubscriptionRequested (this, new SubscriptionRequestEventArgs (contact, message));
		}
		
		protected void OnSubscriptionUpdated (Contact contact, SubscriptionState old_state)
		{
			if (SubscriptionUpdated != null)
				SubscriptionUpdated (this, new SubscriptionUpdateEventArgs (contact, old_state));
		}
		
		protected void OnSubscriptionRequestDeclined (Contact contact)
		{
			if (SubscriptionRequestDeclined != null)
				SubscriptionRequestDeclined (this, new ContactEventArgs (contact));
		}
		
		protected void OnContactUpdated (Contact contact)
		{
			if (ContactUpdated != null)
				ContactUpdated (this, new ContactEventArgs (contact));
		}
		
		protected void OnContactAdded (Contact contact)
		{
			if (ContactAdded != null)
				ContactAdded (this, new ContactEventArgs (contact));
		}
		
		protected void OnContactRemoved (Contact contact)
		{
			if (ContactRemoved != null)
				ContactRemoved (this, new ContactEventArgs (contact));
		}
		
		protected void OnContactAddedToGroup (Contact contact, string group)
		{
			if (ContactAddedToGroup != null)
				ContactAddedToGroup (this, new ContactGroupEventArgs (contact, group));
		}
		
		protected void OnContactRemovedFromGroup (Contact contact, string group)
		{
			if (ContactRemovedFromGroup != null)
				ContactRemovedFromGroup (this, new ContactGroupEventArgs (contact, group));
		}
		
		protected void OnContactNameChanged (Contact contact, string old_name)
		{
			if (ContactNameChanged != null)
				ContactNameChanged (this, new ContactNameEventArgs (contact, old_name));
		}
		
		protected void OnContactsReceived ()
		{
			if (ContactsReceived != null)
				ContactsReceived (this, EventArgs.Empty);
		}
		
		protected void OnRetrievingFailed ()
		{
			if (RetrievingFailed != null)
				RetrievingFailed (this, EventArgs.Empty);
		}
		
		protected void OnContactAvatarChanged (Contact contact, string type, byte[] data)
		{
			if (ContactAvatarChanged != null)
				ContactAvatarChanged (this, new AvatarChangeEventArgs (contact, type, data));
		}
		
		#endregion

		internal Roster (Client cl)
		{
			_client = cl;

			_client.AttachPresenceListener (this, 10);
			
			_client.DefineSetQueryHandler (Namespaces.Roster, delegate (Iq push) {
				if (push.From == null || push.From == _client.UID.Bare () ||
				    push.From == _client.UID) // TODO: To be removed once 2009 spec is approved
					HandlePush (push, false);
				Iq reply = push.Response ();
				reply.To = null; // HACK: Because of strange ejabberd behaviour
				_client.Send (reply);
			});
			
			_client.StatusChanged += delegate(object sender, StatusChangeEventArgs e) {
				if (e.Status == Status.Offline)
					ClearPresences ();
			};
			
			_client.ConnectionStateChanged += delegate(object sender, ConnectionStateEventArgs e) {
				if (e.State == ConnectionState.Disconnected)
					Clear ();
			};
		}
	
		private void HandlePush (Iq push, bool initial)
		{
			Log.Debug ("Processing push.");

			var query = push.FirstChild ("query", Namespaces.Roster);
			if (query == null) {
				Log.Error ("Roster push didn't contain a query child.");
				return;
			}
			
			foreach (var child in query) {
				if (child.Name != "item") continue;
				
				var item = new RosterItem (child);
				var jid = item.Jid;
				
				Log.Debug ("Processing push for " + jid);				
				
				UpdateContact (GetContact (jid), item);
				
				if (!initial) break; // as roster push must contain only one item, ignore the rest
			}
		}
		
		private void UpdateContact (Contact contact, RosterItem item)
		{
			var old_name = contact.Name;
			var old_state = contact.Subscription;
			var old_ask = contact.IsAsking;
								
			contact.Update (item);

			if (old_name != contact.Name)
				OnContactNameChanged (contact, old_name);
			
			if (old_ask && !contact.IsAsking && !contact.IsSubscribed)
				OnSubscriptionRequestDeclined (contact);

			if (old_state != contact.Subscription)
				OnSubscriptionUpdated (contact, old_state);

			OnContactUpdated (contact);
		}

		private void HandlePresenceUpdate (Presence pres)
		{
			Log.Debug ("Handling status update for " + pres.From);
			// TODO: ignore stanzas from unknown entities?
			// TODO: special self-contact handling
			var contact = GetContact (pres.From.Bare ());
			contact.ImportPresence (pres);
			OnPresenceUpdated (contact, pres);
			HandleContactAvatar (contact, pres);
		}

		private void HandleSubscriptionRequest (Presence pres)
		{
			var jid = pres.From.Bare ();
			var contact = GetContact (jid);
			_requests [jid] = pres;
			contact.IsWaiting = true;
			OnSubscriptionRequested (contact, pres.Status);
		}
		
		bool IPresenceListener.HandlePresence (Presence pres)
		{
			switch (pres.Type) {
				case PresType.Available:
				case PresType.Error:
				case PresType.Unavailable:
					HandlePresenceUpdate (pres);
					break;
				case PresType.Subscribe:
					HandleSubscriptionRequest (pres);
					break;
			}
			
			return false;
		}

		private void Clear ()
		{
			foreach (var contact in _items.Values) {
				contact.ClearPresences ();
				var item = new RosterItem (JabberID.Empty);
				item.Subscription = SubscriptionState.Remove;
				
				OnContactUpdated (contact);
				// TODO: cached roster
			}
		}
		
		private void ClearPresences ()
		{
			foreach (var contact in _items.Values) {
				contact.ClearPresences ();
				OnPresenceUpdated (contact, null);
			}
		}
					
		internal void RequestItems ()
		{
			var query = new Iq (IqType.Get, Namespaces.Roster);
			var result = _client.SendQuery (query, -1);
			
			if (result.IsError) {
				Log.Error ("Error in retrieving roster:\n" +
				           result.Error.GetHumanRepresentation () +
				           " (" + result.Error.Description + ")");
				OnRetrievingFailed ();
			}
			else {
				HandlePush (result, true);
				OnContactsReceived ();
			}
		}

		IEnumerator IEnumerable.GetEnumerator ()
		{
			return _items.Values.GetEnumerator ();
		}
		
		public IEnumerator<Contact> GetEnumerator ()
		{
			return _items.Values.GetEnumerator ();
		}

		public Presence GetSubscriptionRequest (JabberID jid)
		{
			return _requests [jid];
		}
		
		public bool ContainsContact (JabberID jid)
		{
			if (jid.IsFull)
				jid = jid.Bare ();
			return _items.ContainsKey (jid);
		}
		
		public void CheckContact (JabberID jid)
		{
			GetContact (jid);
		}
		
		public Contact TryGetContact (JabberID jid)
		{
			Contact contact;
			return _items.TryGetValue (jid.Bare (), out contact) ? contact : null;
		}
		
		public Contact GetContact (JabberID jid)
		{
			jid = jid.Bare ();
			
			Contact contact;
			if (_items.TryGetValue (jid, out contact)) {
				return contact;
			}
			else {
				contact = new Contact (_client, jid);
				_items.Add (jid, contact);
				OnContactAdded (contact);
				return contact;
			}
		}
		
		public void AddContact (JabberID jid, string nick, string group, bool approve_subscription,
		                        bool request_subscription, string request_message)
		{
			jid = jid.Bare ();

			var item = new RosterItem (jid);
			if (!String.IsNullOrEmpty (nick))
				item.Name = nick;
			if (!String.IsNullOrEmpty (group))
				item.Groups.Add (group);

			_client.Send (item.CreatePush ());
			
			if (request_subscription)
				RequestSubscription (jid, request_message);

			if (approve_subscription)
				ApproveSubscription (jid);
		}

		#region groups
		
		public void RenameGroup (string group, string new_name)
		{
			foreach (JabberID jid in new List<JabberID> (_groups [group]))
				RequestContactGroupReplacing (_items [jid], group, new_name);
		}
		
		public void RemoveGroup (string group)
		{
			foreach (JabberID jid in new List<JabberID> (_groups [group]))
				RequestContactGroupRemoval (_items [jid], group);
		}

		public int ContactsInGroup (string group)
		{
			if (!_groups.ContainsKey (group ?? String.Empty))
				return 0;
			return _groups [group ?? String.Empty].Count;
		}
		
		public int OnlineInGroup (string group)
		{
			if (!_groups.ContainsKey (group ?? String.Empty))
				return 0;

			int count = 0;

			foreach (JabberID jid in _groups [group ?? String.Empty])
				if (_items [jid].IsOnline) count ++;

			return count;
		}

		public List<Contact> GetContactsInGroup (string group)
		{
			var list = new List<Contact> ();
			foreach (JabberID jid in _groups [group])
				list.Add (_items [jid]);
			return list;
		}

		internal void AddToGroup (string group, Contact contact)
		{
			if (!_groups.ContainsKey (group))
				_groups [group] = new List <JabberID> ();
			_groups [group].Add (contact.Jid);
			OnContactAddedToGroup (contact, group);
		}
		
		internal void RemoveFromGroup (string group, Contact contact)
		{
			_groups [group].Remove (contact.Jid);
			if (_groups [group].Count == 0)
				_groups.Remove (group);
			OnContactRemovedFromGroup (contact, group);
		}
		
		#endregion
		
		#region server requests

		protected void SendUpdate (RosterItem item)
		{
			_client.SendWithId (item.CreatePush ());
		}

		public void RequestContactNameChange (Contact contact, string name)
		{
			var item = contact.GetItem ();
			item.Name = name;
			SendUpdate (item);
		}

		public void RequestContactGroupsChange (Contact contact,
		                                        IEnumerable<string> new_groups)
		{
			var item = contact.GetItem ();
			item.SetGroups (new_groups);
			SendUpdate (item);
		}

		public void RequestContactGroupAddition (Contact contact, string group)
		{
			var item = contact.GetItem ();
			item.Groups.Add (group);
			SendUpdate (item);
		}

		public void RequestContactGroupRemoval (Contact contact, string group)
		{
			var item = contact.GetItem ();
			item.Groups.Remove (group);
			SendUpdate (item);
		}

		public void RequestContactGroupReplacing (Contact contact,
		                                          string old_group, string new_group)
		{
			var item = contact.GetItem ();
			item.Groups.Remove (old_group);
			item.Groups.Add (new_group);
			SendUpdate (item);
		}

		public void RequestContactRemoval (JabberID jid)
		{
			var contact = GetContact (jid);

			if (contact.IsInRoster) {
				var item = new RosterItem (jid);
				item.Subscription = SubscriptionState.Remove;
				SendUpdate (item);
			} else {
				_items.Remove (jid);
				OnContactRemoved (contact);
			}
		}
		
		#endregion

		#region subscription
		
		public void RequestSubscription (JabberID jid, string message)
		{
			var pres = new Presence (PresType.Subscribe, jid.Bare ());
			pres.AppendStatus (message);
			_client.Send (pres);
		}

		public void ApproveSubscription (JabberID jid)
		{
			_client.Send (new Presence (PresType.Subscribed, jid.Bare ()));
		}

		public void DenySubscription (JabberID jid)
		{
			var contact = GetContact (jid);
			contact.IsWaiting = false;
			_client.Send (new Presence (PresType.Unsubscribed, contact.Jid));
		}

		#endregion
		
		#region Avatars
		
		// called from HandlePresenceUpdate
		private void HandleContactAvatar (Contact contact, Presence pres)
		{
			var elm = pres.FirstChild ("x", Namespaces.vCardUpdate);
			if (elm == null) return; // ignore
			elm = elm.FirstChild ("photo", null);
			if (elm == null) return; // ignore
			
			var hash = elm.Text;
			if (String.IsNullOrEmpty (hash)) hash = null;
			
			string orig_hash;
			if (!_avatar_hashes.TryGetValue (contact.Jid, out orig_hash))
				orig_hash = null;
			
			if (hash != orig_hash) {
				if (hash == null) {
					_avatar_hashes.Remove (contact.Jid);
					OnContactAvatarChanged (contact, null, null);
				}
				
				if (_client.CacheEnabled && _client.CacheHandler.StoresData ("avatars/" + hash)) {
				    byte[] data;
				    _client.CacheHandler.RetrieveData ("avatars/" + hash, out data);
					OnContactAvatarChanged (contact, null, data);
				}
				else GetAvatar (contact);
			}
		}
		
		private void GetAvatar (Contact contact)
		{
			var vcard = new vCard (_client, contact.Jid);
			vcard.Finished += delegate (object sender, EventArgs e) {
				string type, hash;
				byte[] data;
				
				try {
					type = vcard.PhotoType;
					data = Convert.FromBase64String (vcard.PhotoData);
					hash = Cryptography.Sha1HexHash (data);

					Log.Info ("Hash for " + contact.Jid + ": " + hash);
					
					string orig_hash;
					if (_avatar_hashes.TryGetValue (contact.Jid, out orig_hash)
					    && orig_hash == hash) return;
					
					_avatar_hashes [contact.Jid] = hash;
				}
				catch {
					_avatar_hashes.Remove (contact.Jid);
					return;
				}
				
				Log.Info ("Avatar updated for contact {0}, hash: {1}", contact.Jid, hash);
				
				if (_client.CacheEnabled)
					_client.CacheHandler.StoreData ("avatars/" + hash, data);
				
				OnContactAvatarChanged (contact, type, data);
			};
			vcard.Retrieve ();
		}
		
		#endregion
	}
}
